'''
Created on 18/02/2010

@author: eh01
'''
from xml.etree import ElementTree
from string import Template
import re
import httpclient
import myschema
import mywsdl
import os
import types

wsdl_relative_path = '../../MyeDAIS2.3.0r2Artefacts/bcv'
ws_addressing_ns = 'http://www.w3.org/2005/08/addressing'
def generate_soap_message_for_service(wsdl_file_name, operation_name, input_output_fault, sample, required_only, to_uri, mapping=None, spaces=None, context_data=None, reply_to=None):    
    (wsdl, required_schemas) = get_wsdl_and_schemas(wsdl_file_name)
   
    # create soap request message
    env = ElementTree.Element('{http://schemas.xmlsoap.org/soap/envelope/}Envelope')
    
    # create header
    header = ElementTree.SubElement(env, '{http://schemas.xmlsoap.org/soap/envelope/}Header')
    to_element = ElementTree.SubElement(header, '{' + ws_addressing_ns + '}To')
    to_element.text = to_uri
    
    # if a reply_to address is supplied, then add it in the header
    if reply_to:
        reply_to_element = ElementTree.SubElement(header, '{' + ws_addressing_ns + '}ReplyTo')
        address_element = ElementTree.SubElement(reply_to_element, '{' + ws_addressing_ns + '}Address')
        address_element.text = reply_to
        
    action_element = ElementTree.SubElement(header, '{' + ws_addressing_ns + '}Action')
    
    # create body
    body = ElementTree.SubElement(env, '{http://schemas.xmlsoap.org/soap/envelope/}Body')
        
    # start to create the message body
    # get the first element in the body which is the type for this message
    # get the part element for the message for this operation
    if input_output_fault == 'input':
        part_element = wsdl.service.port.port_type.operations[operation_name].input.message.part.element
        action = wsdl.service.port.port_type.operations[operation_name].input.action
    elif input_output_fault == 'output':
        part_element = wsdl.service.port.port_type.operations[operation_name].output.message.part.element
        action = wsdl.service.port.port_type.operations[operation_name].output.action
    elif input_output_fault == 'fault':
        part_element = wsdl.service.port.port_type.operations[operation_name].fault.message.part.element
        action = wsdl.service.port.port_type.operations[operation_name].fault.action
    else:
        raise Exception('Not valid input_ouput_fault value. Must be either "input", "output" or "fault".')
    
    if action is None:
        action = wsdl.service.port.binding.operations[operation_name].soap_action
    action_element.text = action
    
    # load the schema for the element that is referenced in the part
    schema = required_schemas[part_element.namespace]
    # get the type of the element
    type = schema.elements[part_element.name].type
    # get the complex type
    (ct, schema) = get_type(type, required_schemas)
    # add the body element
    e = create_element(body, schema.target_namespace, part_element.name)
    # create all the elements under this body element
    create_complex_type(e, ct, schema, required_schemas, part_element.name, sample, required_only, mapping, spaces, context_data, None)
    # generate the soap message
    soap_message = '<?xml version="1.0" encoding="utf-8" ?>\r\n' + get_elements_as_string(env)
    
    return (soap_message, action)

def create_complex_type(parent_element, complex_type, parent_schema, schemas, path, sample, required_only, mapping, spaces, context_data, base_uri_or_data):
    try:
        # handle complex types with elements
        if complex_type.elements:
            # handle all the attributes
            if complex_type.attributes:
                for attribute in complex_type.attributes.values():
                    create_attributes(parent_element, schemas, path, attribute, sample, required_only, mapping, spaces, context_data,base_uri_or_data)
            # handle all the elements  
            for element_or_choice_element in complex_type.elements:
                # since some elements are 'choice' elements (i.e. elements inside <choice> tags). These are represented in a dictionary.
                if type(element_or_choice_element) is types.DictType:
                    choice = element_or_choice_element
                    if sample:
                        # if we are doing a sample soap message, then just grab the first element in the 'choice' as the one we are going to choose.
                        element_element = choice.values()[0]
                        process_element(parent_element, complex_type, parent_schema, schemas, path, sample, required_only, mapping, spaces, context_data, base_uri_or_data, element_element)
                    else:
                        # go through all the choice elements, and try to create the element. It is now up to the mapping to prevent more than one of the choice elements being mapped and created.
                        for element_element in choice.values():
                            process_element(parent_element, complex_type, parent_schema, schemas, path, sample, required_only, mapping, spaces, context_data, base_uri_or_data, element_element)
                # If it is not a dictionary, then its not a choice element, then just process the element
                else:
                    element_element = element_or_choice_element
                    process_element(parent_element, complex_type, parent_schema, schemas, path, sample, required_only, mapping, spaces, context_data, base_uri_or_data, element_element)
        # complex type with simple content
        else:            
            simple_content = complex_type.simple_content
            # process the attributes
            for attribute in simple_content.attributes.values():
                create_attributes(parent_element, schemas, path, attribute, sample, required_only, mapping, spaces, context_data, base_uri_or_data)
            # the simple content usually extends a simple type.
            (simple_type, schema) = get_type(complex_type.simple_content.extension, schemas)
            # if we are doing a sample message, then populate with sample data. Checking that base_uri_or_data is None because if should not be populated with anything when we 
            # are doing a sample run.
            if sample and not base_uri_or_data:
                # if the simple type is a enum, then get the first value of the enums as the sample value
                if simple_type.enums:
                    sample_attribute_value = simple_type.enums.values()[0]
                    parent_element.text = sample_attribute_value
                # if not enum, then depending on the restriction type, use the following sample data.
                else:
                    restriction_type = simple_type.restriction
                    if restriction_type.namespace == 'http://www.w3.org/2001/XMLSchema' and restriction_type.name == 'string':
#                        parent_element.text = 'test_element_string'
                        parent_element.text = 'test_element_string'
                    elif restriction_type.namespace == 'http://www.w3.org/2001/XMLSchema' and restriction_type.name == 'dateTime':
                        parent_element.text = '2002-05-30T09:30:10Z'
                    elif restriction_type.namespace == 'http://www.w3.org/2001/XMLSchema' and restriction_type.name == 'decimal':
                        parent_element.text = '0.05'
                    elif restriction_type.namespace == 'http://www.w3.org/2001/XMLSchema' and restriction_type.name == 'boolean':
                        parent_element.text = '1'  
                    elif restriction_type.namespace == 'http://www.w3.org/2001/XMLSchema' and restriction_type.name == 'base64Binary':
                        parent_element.text = 'base64string'
                    # if none of the above cases matches the type, then print out the restriction type and add a new case when you 
                    # discover which type is not accommodated.                                        
                    else:
                        print 'The following restriction type is not supported in create_complex_type method: {0}'.format(restriction_type)
            # if we are not doing a sample, then populate with real data from the spaces via the mapping table
            else:
                # check that the path is in the mapping, this is done because if we dont want to map the data, we shouldn't add the data to the element
                if path in mapping:
                    data = base_uri_or_data
                    parent_element.text = data
                else:
                    # anything thats not specified in mapping is acknowledged as a ignored path
                    print 'Path Ignored:{0}'.format(path)
    except RuntimeError:
        print 'Failed on this path: {0}'.format(path)

def process_element(parent_element, complex_type, parent_schema, schemas, path, sample, required_only, mapping, spaces, context_data, base_uri_or_data, element):
    # path of element
    element_path ="/".join([path,element.name])
    # if it is a sample message or if the element_path is in the mapping, then process the element.
    if sample or element_path in mapping:
        if sample:
            # if we are doing a sample and with all elements (i.e. not required_only) then produce at least one of this element
            if element.min_occurs == 0 and not required_only:
                num_of_elements = 1
                # base_uris_or_data only contains None in sample
                base_uris_or_data = [None]
            else:
                num_of_elements = element.min_occurs
                base_uris_or_data = []
                for i in range(num_of_elements):
                    # base_uris_or_data only contains None in sample
                    base_uris_or_data.append(None)
        elif required_only and element.min_occurs == 0:
            # if we are generating only the required element, and this element's minOccurs is zero, then we'll just ignore this element
            return
        else:
            # this list can contain spaces_uris or simple data (e.g strings)
            base_uris_or_data = []
            # get the spaces template uris in the mapping table. 
            spaces_template_uris = mapping[element_path]
            for spaces_template_uri in spaces_template_uris:
                # generate the spaces_uri from the template with the base_uri and context_data
                base_uri = base_uri_or_data
                spaces_uri = generate_uri(spaces_template_uri, base_uri, context_data)
                # get the data from spaces and add it to list. This data can be either another spaces_uri or simple data
                base_uris_or_data.append(spaces[spaces_uri])
            num_of_elements = len(base_uris_or_data)
        # num_of_elements indicates the number of instances we are going to generate for this element 
        for i in range(num_of_elements):
            (element_complex_type, schema) = get_type(element.type, schemas)
            # if there is a complex_type definition in the schemas and it isn't the same complex_type we have now if we are doing a sample message
            if element_complex_type and (not sample or (element_complex_type != complex_type and sample)):
                e = create_element(parent_element, parent_schema.target_namespace, element.name)                                    
                create_complex_type(e, element_complex_type, schema, schemas, element_path, sample, required_only, mapping, spaces, context_data, base_uris_or_data[i])
            elif element.type.namespace == 'http://www.w3.org/2001/XMLSchema' and element.type.name == 'anyType':
                e = create_element(parent_element, parent_schema.target_namespace, element.name)
            elif element.type.namespace == 'http://www.w3.org/2001/XMLSchema' and element.type.name == 'anyURI':
                e = create_element(parent_element, parent_schema.target_namespace, element.name)
                e.text = 'testURI'
    # if there is no mapping for the element, but it is required. Then print out a message to indicate it is required.             
    elif element.min_occurs > 0:
        # required path which has not been populated
        raise Exception('Required Path Ignored:{0}'.format(element_path))
    # if we are doing only the required elements, then print out a message to indicate that this path was ignored, due to no mapping for this path.
    elif not required_only:
        # anything thats not specified in mapping is acknowledged as a ignored path
        print 'Path Ignored:{0}'.format(element_path)
                    
def create_attributes(parent_element, schemas, path, attribute, sample, required_only, mapping, spaces, context_data, base_uri_or_data):
    if not attribute.use and required_only:
        # if we are only doing the required attributes and this one is optional, then dont do anything
        return
    # attribute path
    attribute_path ="@".join([path,attribute.name])
    attribute_type = attribute.type
    (simple_type, schema) = get_type(attribute_type, schemas)
    # if we are doing a sample, then use sample data
    if sample:
        # attributes can have simple_type types
        if simple_type:
            # if the simple_type is a enum, then use the first enum as the example value
            if simple_type.enums:
                sample_attribute_value = simple_type.enums.values()[0]
                parent_element.text = sample_attribute_value
            # if the simple type is not a enum, the use the sample data below depending on the type.
            else:
                if attribute_type.namespace == 'http://www.w3.org/2001/XMLSchema' and attribute_type.name == 'string':
                    parent_element.attrib[attribute.name] = 'test_attribute_string'
        # if the attribute is not a simple type                            
        else:
            # if there is a default value, then use it as the sample value
            if attribute.default:
                parent_element.attrib[attribute.name] = attribute.default
            # use a sample data below, depending on the type
            elif attribute_type.namespace == 'http://www.w3.org/2001/XMLSchema' and attribute_type.name == 'string':
                parent_element.attrib[attribute.name] = 'test_attribute_string'
            # otherwise if the type is not supported, then print it out below.
            else:
                print 'The following type is not supported in create_attribute method: {0}'.format(attribute.type)
    # if not sample then use the actual data
    else:
        # if the attribute_path is in the mapping
        if attribute_path in mapping:
            # get the data from spaces and set it on the element
            spaces_template_uri = mapping[attribute_path]
            spaces_uri = generate_uri(spaces_template_uri, base_uri_or_data, context_data)
            data = spaces[spaces_uri]
            parent_element.attrib[attribute.name] = data
        # if there is no mapping for the attribute, but it is required. Then print out a message to indicate it is required. 
        elif attribute.use:
            print 'Required Path Ignored:{0}'.format(attribute_path)
        # if we are doing only the required attributes, then print out a message to indicate that this path was ignored, due to no mapping for this path.            
        elif required_only:
            print 'Path Ignored:{0}'.format(attribute_path)

def create_element(parent_element, target_namespace, name):
    return ElementTree.SubElement(parent_element, '{0}{1}'.format("{" + target_namespace + "}", name))

def get_elements_as_string(root_element):
    class dummy:
        pass    
    data = []
    file = dummy()
    file.write = data.append
    ElementTree.ElementTree(root_element).write(file, None)
    return "".join(data)

def get_type(type, schemas):
    # find the schema that contains this type
    if type.namespace in schemas:
        schema = schemas[type.namespace]
        # if it is a complex type
        if type.name in schema.complex_types:
            # return the type and the schema it is defined in.
            return (schema.complex_types[type.name], schema)
        # if it is a simple type
        elif type.name in schema.simple_types:
            # return the type and the schema it is defined in.
            return (schema.simple_types[type.name], schema)
    # if no schema is found, return that we didn't find the type definition, and the schema it belongs to.
    else:
        return (None, None)
    
def get_imported_schema(schema, current_dir, schemas):
    for imp in schema.imports:
        if imp.namespace in schemas:
            # if the schema is already in the schemas list, then skip 
            continue
        else:
            # parse and add schema to schemas list.
            schema_path = os.path.join(current_dir, imp.schema_location)
            schema = myschema.parse(schema_path)
            schemas[schema.target_namespace] = schema
            # recursively import all the schemas used by this schema
            get_imported_schema(schema, os.path.split(schema_path)[0], schemas)

def generate_uri(template_uri, base_uri, context_data):
    # template_uri has parameters within it.
    # base_uri is a parent of the template_uri.
    # E.g: base_uri = 'a/b/c/d, template_uri = 'a/$param/c/d/e
    # with the base_uri it is possible to figure out template param values that are 
    # part of the subset part of the template_uri. In the example above $param = 'b'
    
    # you can pass context data into the template to populate the template. In scenarios 
    # where a template param cannot be figured out like from the example above, passing the 
    # param through the context_data dict would solve the problem. This is usually the scneario 
    # where you might not have some the data. 
    # Any template param that has not been populated, will be pattern matched with the 
    # base uri.
    template = Template(template_uri)
    template_uri = template.safe_substitute(context_data) 
    # if a base_uri is provided
    if base_uri:
        generated_uri = []
        # split both uris on slashes
        template_uri_parts = template_uri.split('/')
        base_uri_parts = base_uri.split('/')
        # going through all the parts of the template uri
        for i in range(len(template_uri_parts)):
            # if a part starts with a $ symbol, then it is a parameter
            if template_uri_parts[i].startswith('$'):
                # get the corresponding part on the base uri and substitute it as the value for that parameter in the generated uri list.
                generated_uri.append(base_uri_parts[i])
            # if it is not a parameter, then just add it to the generated uri list.
            else:
                generated_uri.append(template_uri_parts[i])
        # join all the generated_uri parts in the list as the generated uri
        return '/'.join(generated_uri)
    # if no base_uri, then it is assumed that the template_uri has no more parameters. So just return the template_uri.
    else:
        return template_uri

def get_wsdl_and_schemas(wsdl_file_name):
    # get the wsdl
    wsdl = mywsdl.parse(os.path.join(wsdl_relative_path, wsdl_file_name))
    # get all the schemas involved
    schema = wsdl.wsdl_type.schema
    required_schemas = {}
    get_imported_schema(schema, wsdl_relative_path, required_schemas)
    return (wsdl, required_schemas)

def get_ns_and_name(element):
    tag_pattern = re.compile(r'{(.*?)}(.*)')
    matches = tag_pattern.match(element.tag)
    return (matches.group(1), matches.group(2))

def get_child_elements(parent_element, namespace, child_element_name):
    return parent_element.findall('./{' + namespace + '}' + child_element_name)

def parse_soap_message(wsdl_file_name, soap_message, mapping):
    '''
    Finds the wsdl and schemas and parses the soap message.
    '''
    extracted_content = {}
    (wsdl, required_schemas) = get_wsdl_and_schemas(wsdl_file_name)
    # parse into element tree
    envelope_element = ElementTree.XML(soap_message)
    
    # parse the header element. Since we do not parse the schema for SOAP or WS-Addressing,
    # we cannot parse out the reply_to_address with the 
    # parse_complex_type_instance_base_on_complex_type_definition method. Since we only 
    # want the reply_to_address in the soap header, this implementation is fine for a POC.
    header_element = envelope_element.find('./{http://schemas.xmlsoap.org/soap/envelope/}Header')
    # set default to None
    reply_to_address = None
    if header_element:
        reply_to_element = header_element.find('./{' + ws_addressing_ns + '}ReplyTo')
        if reply_to_element:
            address_to_element = reply_to_element.find('./{' + ws_addressing_ns + '}Address')
            reply_to_address = address_to_element.text
    body_element = envelope_element.find('./{http://schemas.xmlsoap.org/soap/envelope/}Body')
    message_element = body_element.getchildren()[0]
    
    # start parsing at the message element
    (ns, name) = get_ns_and_name(message_element)
    element_path = name
    schema = required_schemas[ns]
    type = schema.elements[name].type
    (ct, schema) = get_type(type, required_schemas)
    parse_complex_type_instance_based_on_complex_type_definition(message_element, ct, schema, required_schemas, mapping, element_path, extracted_content)
    return (extracted_content, reply_to_address)

def parse_complex_type_instance_based_on_complex_type_definition(parent_element, parent_element_type, parent_element_type_schema, required_schemas, mapping, element_path, extracted_content):
    '''
    Parses the attributes and elements of an instance of an complex type. This is done through going through the complex type
    definition and seeing if the attribute or element exists in the instance before we parse it.
    '''    
    # parse all attributes of the complex type
    for attribute in parent_element_type.attributes.values():
        attribute_value = None
        new_element_path = "@".join([element_path, attribute.name])
        if attribute.name in parent_element.attrib.keys():
            attribute_value = parent_element.attrib[attribute.name].strip()
        if new_element_path in mapping:
            extracted_content[new_element_path] = attribute_value
            # right now not doing anything useful than just printing it
            print '{0} = {1}'.format(new_element_path, attribute_value)
    # go through all the elements in the complex type and parse these elements if they exist in the instance of the complex type
    for element_or_choice_element in parent_element_type.elements:
        # handle choice elements. No real checking here that only one of the choice element exists
        if type(element_or_choice_element) is types.DictType:
            for element in element_or_choice_element.values():
                parse_complex_type_instance_element_content(parent_element, parent_element_type_schema, required_schemas, mapping, element, element_path, extracted_content)
        else:
            element = element_or_choice_element
            parse_complex_type_instance_element_content(parent_element, parent_element_type_schema, required_schemas, mapping, element, element_path, extracted_content)
        
def parse_complex_type_instance_element_content(parent_element, parent_element_type_schema, required_schemas, mapping, element, element_path, extracted_content):
    '''
    Parses the content of every element in parent_element that is of particular element in the complex type definition
    '''
    # new path to current element
    new_element_path = "/".join([element_path, element.name])
    type = element.type
    (ct, schema) = get_type(type, required_schemas)
    if new_element_path in mapping:
        extracted_content[new_element_path] = []
    # get all the child elements of a particular element for this parent element
    child_elements = get_child_elements(parent_element, parent_element_type_schema.target_namespace, element.name)
    for child_element in child_elements:
        # we are interested in element if its in the mapping.
        if new_element_path in mapping:
            if child_element.text and child_element.text.strip():
                extracted_content[new_element_path].append(child_element.text.strip())
            # right now not doing anything useful than just printing it
            print '{0} = {1}'.format(new_element_path, child_element.text)
        # get type of element
        # if there is a complex type definition for this element, the parse the element further
        if ct:
            parse_complex_type_instance_based_on_complex_type_definition(child_element, ct, schema, required_schemas, mapping, new_element_path, extracted_content)

if __name__ == '__main__': 
    wsdl_file_name = 'ChangeReferral_Initiator.2.3.0r2.wsdl'
    operation_name = 'Accept'
    (soap_message, soap_action) = generate_soap_message_for_service(wsdl_file_name, operation_name, 'input', True, True, 'http://localhost:8080', None, None, None)
#    soap_message = generate_soap_message_for_service(wsdl_file_name, operation_name, True, True, mysoaptest.mapping, mysoaptest.spaces_dummy, {'spear_ref':'S000001E'})
    print soap_message
#    http_headers = {'Content-Type':'text/xml;charset=UTF-8',
#                    'SOAPAction':  soap_action}
##    (code, response) = httpclient.send_request('ed.yarra-project.com', '80', soap_message)
#    (code, response) = httpclient.send_request('http://localhost:4040/eDAISNCP/services/Responder_CreateApplicationTransaction_Service.2.3.0r2', soap_message, http_headers)
#    print response
#    print code